package de.mmis.core.sexpression.util.gui;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.TreeMap;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.table.AbstractTableModel;

import org.apache.log4j.Logger;

import de.mmis.core.base.MethodDescriptor;
import de.mmis.core.base.ObjectDescriptor;
import de.mmis.core.base.abstracttree.InnerNode;
import de.mmis.core.base.abstracttree.LeafNode;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.abstracttree.TreeParserException;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper.MethodInvocationException;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper.MethodReturnValue;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper.NoMatchingMethodFoundException;
import de.mmis.core.base.device.Device;
import de.mmis.core.base.event.GenericObserver;
import de.mmis.core.base.event.GenericObserverEvent;
import de.mmis.core.base.event.Observable;
import de.mmis.core.base.event.Observer;
import de.mmis.core.base.gclass.GArray;
import de.mmis.core.base.gclass.GClass;
import de.mmis.core.base.gclass.GParameterized;
import de.mmis.core.base.gui.AbstractControlComponent;
import de.mmis.core.base.gui.DeviceControlComponent;
import de.mmis.core.base.gui.GUIComponent;
import de.mmis.core.sexpression.SExpressionParser;

/**
 * default {@link DeviceControlComponent} used for all types of {@link Device}.
 * It uses {@link Tree}s to describe method calls
 * 
 * @author Christian Koch
 * 
 */
public class DefaultControlComponent extends AbstractControlComponent<Object> {

	private final static Logger LOGGER = Logger
			.getLogger(DefaultControlComponent.class);

	private static class Error extends Exception {

		public Error(String message) {
			super(message);
		}
	}

	public DefaultControlComponent() {
		super(0, Object.class, "Default");
	}

	/**
	 * table model for displaying table of all properties (i.e. getter methods)
	 * 
	 * @author Martin Nyolt
	 */
	private static class PropertyListTableModel extends AbstractTableModel {

		private String[] columnNames = new String[] { "Methode", "Ergebnis" };

		private ArrayList<String[]> data = new ArrayList<String[]>();

		public PropertyListTableModel() {
			// doing nothing
		}

		public void addProperty(String method, String result) {
			data.add(new String[] { method, result });
		}

		@Override
		public int getColumnCount() {
			return columnNames.length;
		}

		@Override
		public String getColumnName(int col) {
			return columnNames[col];
		}

		@Override
		public int getRowCount() {
			return data.size();
		}

		@Override
		public Object getValueAt(int row, int col) {
			return data.get(row)[col];
		}

		@Override
		public boolean isCellEditable(int row, int col) {
			if (col == 1)
				return true;
			return false;
		}
	}

	/**
	 * table model for displaying table of all events occurred (if device is
	 * {@link Observable})
	 * 
	 * @author Christian Koch
	 */
	private static class EventListTableModel extends AbstractTableModel {
		private static final int MAXIMUM_NUMBER_OF_EVENTS_STORED = 1000;

		private String[] columnNames = new String[] { "Zeit", "Event" };

		public ArrayList<Object[]> data = new ArrayList<Object[]>();

		public EventListTableModel() {
			// doing nothing
		}

		public void addEvent(Object event, String eventtype) {
			Date now = new Date();

			if (data.size() >= MAXIMUM_NUMBER_OF_EVENTS_STORED)
				// limit events stored
				data.remove(0);
			data.add(new Object[] { now, eventtype + ": " + event.toString() });
			fireTableRowsInserted(data.size() - 1, data.size() - 1);
		}

		@Override
		public int getColumnCount() {
			return columnNames.length;
		}

		@Override
		public String getColumnName(int col) {
			return columnNames[col];
		}

		@Override
		public int getRowCount() {
			return data.size();
		}

		@Override
		public Object getValueAt(int row, int col) {
			return data.get(row)[col];
		}

		@Override
		public boolean isCellEditable(int row, int col) {
			if (col == 1)
				return true;
			return false;
		}
	}

	private JLabel methodLabel;
	private JComboBox methodComboBox;
	/** This Panels contains one panel, namely of the current selected method */
	private JPanel methodPanel;

	private JButton methodCallButton;
	private JTextField methodCallTextField;
	private JLabel methodResultLabel;
	private JTextField methodResultTextField;

	private JTable propertyTable;
	private JTable eventTable;
	private EventListTableModel eventTableModel;

	private ObjectDescriptor objectDescriptor;
	private GenericObserver<?> genericObserver;

	private Tree callMethod(Tree methodCall) throws Error {
		if (!getDevice().tryEnsureAvailability())
			throw new Error("device not available!");

		Tree result;
		try {
			MethodReturnValue mrv = SerializerHelper.invokeObjectMethod(
					objectDescriptor, methodCall, classLoader,
					new HashMap<String, Object>());

			result = mrv.toTree(true);
			if (result == null)
				result = new LeafNode(":void");

		} catch (MethodInvocationException e) {
			e.printStackTrace();
			throw new Error("#ERROR: Fehler beim Aufruf der Methode");
		} catch (NoMatchingMethodFoundException e) {
			e.printStackTrace();
			throw new Error("#ERROR: Methodenname nicht gefunden");
		} finally {
			getDevice().releaseAvailability();
		}

		return result;
	}

	@Override
	public void initialize(Class<?> clazz) {
		objectDescriptor = new ObjectDescriptor(getDevice().getDevice());
		genericObserver = GenericObserver.createGenericObserver(
				objectDescriptor, true);

		if (genericObserver != null) {
			eventTableModel = new EventListTableModel();

			genericObserver.addObserver(new Observer<GenericObserverEvent>() {
				@Override
				public void notify(
						Observable<? extends GenericObserverEvent> sender,
						GenericObserverEvent event) {
					eventTableModel.addEvent(event.getContent(), "DeviceEvent");
					updateProperties();
				}
			});
		}

		initGUI();
		updateProperties();
	}

	static String classToSimpleString(GClass c) {
		if (c instanceof GParameterized) {
			GParameterized clazz = (GParameterized) c;
			String result = clazz.getBaseClass().getSimpleName();
			GClass[] cs = clazz.getTypeParameterClass();

			if (cs.length > 0) {
				result += "<";
				for (GClass cl : cs)
					result += classToSimpleString(cl);
				result += ">";
			}

			return result;
		} else if (c instanceof GArray) {
			return classToSimpleString(((GArray) c).getComponentType()) + "[]";
		} else
			return c.toString();
	}

	static String methodSimpleString(MethodDescriptor md) {
		StringBuilder sb = new StringBuilder();

		sb.append("(");
		sb.append(md.getMethod().getName());

		for (int i = 0; i < md.getArgumentCount(); i++) {
			sb.append(" ");
			sb.append((md.getArgumentName(i) != null) ? md.getArgumentName(i)
					: "?");
			sb.append(":" + classToSimpleString(md.getParameterType(i)));
		}

		sb.append("):");
		sb.append(classToSimpleString(md.getReturnType()));

		return sb.toString();
	}

	private void initGUI() {
		{
			// three JPanels, one below the other: Methods, Properties, Events
			GridBagLayout layout = new GridBagLayout();
			if (genericObserver != null) {
				layout.rowWeights = new double[] { 0.0, 0.5, 0.5 };
				layout.rowHeights = new int[] { 1, 1, 1 };
			} else {
				layout.rowWeights = new double[] { 0.0, 1.0 };
				layout.rowHeights = new int[] { 1, 1 };
			}
			layout.columnWeights = new double[] { 1.0 };
			layout.columnWidths = new int[] { 1 };
			setLayout(layout);
		}
		final Insets insets = new Insets(5, 5, 5, 5);

		JPanel methodsPanel = new JPanel();
		methodsPanel.setBorder(BorderFactory.createTitledBorder("Methoden"));
		add(methodsPanel, new GridBagConstraints(0, 0, 1, 1, 0, 0,
				GridBagConstraints.CENTER, GridBagConstraints.BOTH, insets, 0,
				0));
		// methods are added below

		{
			JPanel fieldPanel = new JPanel();
			fieldPanel.setLayout(new BorderLayout());
			fieldPanel.setBorder(BorderFactory
					.createTitledBorder("Eigenschaften"));
			add(fieldPanel, new GridBagConstraints(0, 1, 1, 1, 0, 0,
					GridBagConstraints.CENTER, GridBagConstraints.BOTH, insets,
					0, 0));

			propertyTable = new JTable(new PropertyListTableModel());
			propertyTable.setPreferredScrollableViewportSize(new Dimension(100,
					100));
			JScrollPane tableScrollPane = new JScrollPane(propertyTable);
			fieldPanel.add(tableScrollPane);
		}

		if (genericObserver != null) {
			JPanel eventsPanel = new JPanel();
			eventsPanel.setLayout(new BorderLayout());
			eventsPanel.setBorder(BorderFactory.createTitledBorder("Events"));
			add(eventsPanel, new GridBagConstraints(0, 2, 1, 1, 0, 0,
					GridBagConstraints.CENTER, GridBagConstraints.BOTH, insets,
					0, 0));

			eventTable = new JTable(eventTableModel);
			eventTable.setPreferredScrollableViewportSize(new Dimension(100,
					100));
			JScrollPane tableScrollPane = new JScrollPane(eventTable);
			eventTable.setFillsViewportHeight(true);
			eventsPanel.add(tableScrollPane);
		}

		GridBagLayout methodsLayout = new GridBagLayout();
		methodsLayout.rowWeights = new double[] { 0.0, 1.0, 0.0 };
		methodsLayout.rowHeights = new int[] { 1, 1, 1 };
		methodsLayout.columnWeights = new double[] { 0.0, 1.0 };
		methodsLayout.columnWidths = new int[] { 1, 1 };
		methodsPanel.setLayout(methodsLayout);
		// first component: Method selector
		// second: method panel
		// third: result

		// first create each method panel
		final TreeMap<String, JPanel> methodPanels = new TreeMap<String, JPanel>();
		{
			JPanel callPanel = new JPanel();
			methodPanels.put("", callPanel);
			GridBagLayout layout = new GridBagLayout();
			layout.rowWeights = new double[] { 1.0 };
			layout.rowHeights = new int[] { 1 };
			layout.columnWeights = new double[] { 0.0, 1.0 };
			layout.columnWidths = new int[] { 1, 1 };
			callPanel.setLayout(layout);

			methodCallButton = new JButton("aufrufen");
			methodCallButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					try {
						Tree methodCall = SExpressionParser
								.parseUTF8(methodCallTextField.getText());
						Tree result = callMethod(methodCall);
						methodResultTextField.setText(result.toString());
					} catch (TreeParserException e1) {
						methodResultTextField
								.setText("#ERROR: Kann S-Expression nicht parsen");
						e1.printStackTrace();
					} catch (Error e1) {
						methodResultTextField.setText(e1.getMessage());
					}
				}
			});
			callPanel.add(methodCallButton, new GridBagConstraints(0, 0, 1, 1,
					0, 0, GridBagConstraints.CENTER,
					GridBagConstraints.HORIZONTAL, insets, 0, 0));

			methodCallTextField = new JTextField();
			callPanel.add(methodCallTextField, new GridBagConstraints(1, 0, 1,
					1, 0, 0, GridBagConstraints.CENTER,
					GridBagConstraints.HORIZONTAL, insets, 0, 0));
		}

		for (final MethodDescriptor md : objectDescriptor
				.getMethodDescriptors()) {
			if (objectDescriptor.getGetterMethodDescriptors().contains(md))
				continue;

			JPanel mp = new JPanel();
			methodPanels.put(methodSimpleString(md), mp);
			GridBagLayout methodLayout = new GridBagLayout();
			// first row: Parameter description
			// second row: button + text fields
			methodLayout.rowWeights = new double[] { 0.5, 0.5 };
			methodLayout.rowHeights = new int[] { 1, 1 };
			methodLayout.columnWeights = new double[md.getArgumentCount() + 1];
			methodLayout.columnWidths = new int[md.getArgumentCount() + 1];

			if (md.getArgumentCount() == 0)
				methodLayout.columnWeights[0] = 1.0;
			else
				methodLayout.columnWeights[0] = 0.0;

			for (int i = 1; i < methodLayout.columnWeights.length; i++)
				methodLayout.columnWeights[i] = 1.0 / md.getArgumentCount();

			mp.setLayout(methodLayout);

			// add first row
			for (int i = 0; i < md.getArgumentCount(); i++) {
				String param = md.getArgumentName(i) + ": "
						+ classToSimpleString(md.getParameterType(i));
				JLabel paramLabel = new JLabel(param);
				paramLabel.setToolTipText(md.getArgumentHelp(i));

				mp.add(paramLabel, new GridBagConstraints(i + 1, 0, 1, 1, 0, 0,
						GridBagConstraints.CENTER,
						GridBagConstraints.HORIZONTAL, insets, 0, 0));
			}

			// add Textfields
			final JTextField[] params = new JTextField[md.getArgumentCount()];
			for (int i = 0; i < md.getArgumentCount(); i++) {
				JTextField field = new JTextField();
				params[i] = field;
				mp.add(field, new GridBagConstraints(i + 1, 1, 1, 1, 0, 0,
						GridBagConstraints.CENTER,
						GridBagConstraints.HORIZONTAL, insets, 0, 0));
			}

			// add button to second row
			JButton callButton = new JButton(md.getName());
			callButton.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					InnerNode methodCall = new InnerNode();
					methodCall.add(new LeafNode(md.getName()));

					for (int i = 0; i < md.getArgumentCount(); i++) {
						try {
							Tree param = SExpressionParser.parseUTF8(params[i]
									.getText());
							methodCall.add(param);
						} catch (TreeParserException e1) {
							methodResultTextField
									.setText("#ERROR: Kann S-Expression vom Parameter "
											+ md.getArgumentName(i)
											+ " nicht parsen");
							LOGGER.error(e1);
							return;
						}
					}

					try {
						Tree result = callMethod(methodCall);
						methodResultTextField.setText(result.toString());
					} catch (Error e1) {
						methodResultTextField.setText(e1.getMessage());
					}
				}
			});
			mp.add(callButton, new GridBagConstraints(0, 1, 1, 1, 0, 0,
					GridBagConstraints.WEST, GridBagConstraints.NONE, insets,
					0, 0));
		}

		{
			methodLabel = new JLabel("Methode:");
			methodsPanel.add(methodLabel, new GridBagConstraints(0, 0, 1, 1, 0,
					0, GridBagConstraints.CENTER,
					GridBagConstraints.HORIZONTAL, insets, 0, 0));

			String[] methodNames = new String[methodPanels.size()];
			Iterator<String> it = methodPanels.keySet().iterator();
			for (int i = 0; it.hasNext(); i++)
				methodNames[i] = it.next();

			methodComboBox = new JComboBox(methodNames);
			methodComboBox.setEditable(false);
			methodComboBox.addActionListener(new ActionListener() {
				@Override
				public void actionPerformed(ActionEvent e) {
					String method = (String) methodComboBox.getSelectedItem();
					JPanel panel = methodPanels.get(method);
					if (panel != null) {
						methodPanel.removeAll();
						methodPanel.add(panel, new GridBagConstraints(0, 0, 1,
								1, 0, 0, GridBagConstraints.CENTER,
								GridBagConstraints.HORIZONTAL, insets, 0, 0));
						revalidate();
					}
				}
			});
			methodsPanel.add(methodComboBox, new GridBagConstraints(1, 0, 1, 1,
					0, 0, GridBagConstraints.CENTER,
					GridBagConstraints.HORIZONTAL, insets, 0, 0));
		}

		{
			methodPanel = new JPanel();
			GridBagLayout layout = new GridBagLayout();
			layout.rowWeights = new double[] { 1.0 };
			layout.rowHeights = new int[] { 1 };
			layout.columnWeights = new double[] { 1.0 };
			layout.columnWidths = new int[] { 1 };
			methodPanel.setLayout(layout);

			methodPanel.add(methodPanels.get(""), new GridBagConstraints(0, 0,
					1, 1, 0, 0, GridBagConstraints.CENTER,
					GridBagConstraints.HORIZONTAL, insets, 0, 0));
			methodsPanel.add(methodPanel, new GridBagConstraints(0, 1, 2, 1, 0,
					0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
					insets, 0, 0));
		}

		{
			methodResultLabel = new JLabel("Ergebnis: ");
			methodsPanel.add(methodResultLabel, new GridBagConstraints(0, 2, 1,
					1, 0, 0, GridBagConstraints.CENTER,
					GridBagConstraints.HORIZONTAL, insets, 0, 0));

			methodResultTextField = new JTextField();
			methodResultTextField.setEditable(false);
			methodsPanel.add(methodResultTextField, new GridBagConstraints(1,
					2, 1, 1, 0, 0, GridBagConstraints.CENTER,
					GridBagConstraints.HORIZONTAL, insets, 0, 0));
		}

	}

	void updateProperties() {
		PropertyListTableModel properties = new PropertyListTableModel();

		for (MethodDescriptor md : objectDescriptor
				.getGetterMethodDescriptors()) {
			InnerNode methodCall = new InnerNode(new LeafNode(md.getName()));
			try {
				Tree result = callMethod(methodCall);
				properties.addProperty(md.getName(), result.toString());
			} catch (Error e) {
				properties.addProperty(md.getName(), e.getMessage());
			}
		}

		propertyTable.setModel(properties);
	}

	@Override
	public void notify(Observable<? extends GUIEvent> sender, GUIEvent event) {
		String message = "GUIEvent";
		if (sender != null)
			message += " from " + ((GUIComponent<?>) sender).toString();

		if (eventTableModel != null)
			eventTableModel.addEvent(Tree.fromObject(event, true), message);

		updateProperties();
	}
}
